
import { Sprite, ZRenderComp } from '../../../common/component/zrComp';
import { cubicInOut, cubicOut, hold, delay, calc, cubicIn } from '../../../common/animation/index';
import { parseColorText } from '../../../common/color/color';

import layout from '../../compute/v1/layout';
import { Cursor, load, Meter, NaviMap } from '../../compute/v1/naviCursor';

import { AbstractContent } from './content';

// 放 image 的, 静态部分
const Z_IMG = 32;
// 放指针、指针上面的坐标数值, 动态部分. 另外 TopTitle 的底在动时也会移到这层
const Z_POINTER = 33;

// UI 图层, 静态部分
const Z_UI_STATIC = 42;
// UI 图层, 动态部分
const Z_UI_DYNAMIC = 43;

const cLayout = {
  topTitle : {
    x : 262, y : 40, height : 64, width : 360,

    rect1 : {
      h1 : 40, h2 : 20, marginR : 20,
    },
  },
  map : {
    view : {
      x : 100, y : 80,
      width : layout.content.width - 200,
      height : layout.content.width - 280,
    },
    intsPt : {
      width: 18,
    },
    thumbnail : {
      width: 240, height: 90,
      lineLPts: [ [10, 20], [30, 0], [58, 0] ], // 以框包围盒左上角位置为锚点
      lineL1W: 1,
      lineL2W: 4,
      // 启动阶段, line 的锚点位置变化. 数值是相对于框中心点
      lineLAnchorPoses: [ [-120, -10], [-39, -10], [-120, -45] ],
      framePath1: 'M-112-23h4l-1,1h-3l-7,7v21l4,4v10l-3,3v11l9,9h19l2-2h3l-4,4h-21l-10-10v-50Z' +
        'M62,42l-3,3h-115l-4-4h4l3,3h112l2-2Z' +
        'M112,23l8-8v-50l-10-10h-15v4h1v-3h12l10,10v20l1,1v28l-7,7h-4l-1,1Z',
      framePath2: (/** @type {number} */ seqWidth) => `M-60-45l-3,3h1l2-2h${150-seqWidth}v3h1v-4Z`,
      frameBgPath: 'M-90-45h200l10,10v50l-30,30h-200l-10-10v-50Z',
    },
  },
  detail : {
    y : layout.content.height - 160,
    tranX : 500,
    freqIdc : {
      x : 144, y: 36,
      rectH : 54,
    },
    resDetect : {
      x : layout.content.width / 2 + 500, y: 84,
      hexH : 18, hexW : 30, hexDx : 36, hexDy : 24,
    }
  },
};

const cEvent = {
  pointMove : 'navi/point/move',
  pointArrive : 'navi/point/arrive',
};

/*
NaviContent (new canvas)
| 无内容
├── MapFragments
│   │ 地图图案, zlevel=Z_IMG (静态时)  zlevel=Z_POINTER (动态时), z=1~4
│   ├── Pointer
│   │     指针, Z_POINTER, 运动的 z=50, 其它 z=20
│   ├── ThumbnailFrame
│   │     画框, 指针指到之后显示地点信息, Z_IMG/Z_POINTER, z=40~59
│   └── InterestPoint
|         地图上的兴趣点, Z_IMG/Z_POINTER, z=10~29
├── TopTitle
│     上面的标题部分. Z_UI_STATIC, z=101~121; Z_UI_DYNAMIC, z=0~10|101~121; Z_POINTER, z=999 (仅背景部分)
├── NaviDetail
│   │ 下面的内容栏. Z_UI_STATIC, z=30~40
│   ├── FreqIndicator
│   │     显示频率部分图表. Z_UI_STATIC, z=60~70, Z_UI_DYNAMIC, z=60~80
│   └── ResourceDetector
│       │ 显示资源分布的动画. Z_UI_DYNAMIC, z=11~40
│       └── ResourceHexagon
│             资源分布的动画中的六边形. Z_UI_DYNAMIC, z=41~50
*/
class NaviContent extends AbstractContent {
  constructor(parent, zrender, animation) {
    super(parent, zrender, animation);
    this.zlevels.push(Z_IMG, Z_POINTER, Z_UI_STATIC, Z_UI_DYNAMIC);
    global.t = this; // TODO
  }

  init() {
    super.init();

    const { topTitle } = cLayout;
    this.topTitle = new TopTitle({
      parent : this,
      x : topTitle.x, y : topTitle.y,
      height : topTitle.height, width : topTitle.width,
    });

    load(this.getContext())
        .then(navi => {
          this.navi = navi;
          this.onloaded();
        });
  }

  onloaded() {
    const { navi } = this;
    const { width: vw, height: vh } = cLayout.map.view;

    const cursor = this.cursor = new Cursor();
    cursor.setImageAndView(navi.image, vw, vh);
    cursor.setInterestPts(navi.interestPts);
    cursor.setCursor(navi.initPt.x * cursor.imgScale, navi.initPt.y * cursor.imgScale);

    this.spFrag = new MapFragments({
      parent : this, deskAnimation: this.deskAnimation,
      naviMap : navi, cursor,
    });
    this.bottom = new NaviDetail({
      parent : this, cursor, deskAnimation: this.deskAnimation,
    });
  }
}

/**
 * 包含: 背景地图、图上的点 (InterestPoint)、指针 (Pointer)
 */
class MapFragments extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    /** @type {NaviMap} */ this.naviMap = args.naviMap;
    /** @type {Cursor} */ const cursor = this.cursor = args.cursor;

    const { x: vx, y: vy } = cLayout.map.view;
    cursor.calcViewPosition();
    this.useParentZRenderInstance(cursor.viewX + vx, cursor.viewY + vy);
    this.destroyed = false;
    this.deskAnimation = args.deskAnimation;

    this.pointer = new Pointer({
      parent : this, cursor,
    });
    this.thumbnail = new ThumbnailFrame({
      parent: this, cursor,
    });
    this.selectCount = 0;
  }

  init() {
    const { x: vx, y: vy } = cLayout.map.view;
    const { cursor } = this;

    // 摆放地图
    this.ticker(() => {
      cursor.calcViewPosition();
      this.x = cursor.viewX + vx;
      this.y = cursor.viewY + vy;
    });
    this.putMap();
    this.putInterestPts();
  }

  putMap() {
    const { naviMap, cursor } = this;
    const { colors } = layout;
    const spBgD3 = this.createPathFromString(naviMap.pathD3)
        .fill(colors.strokeBgD3)
        .scale(cursor.imgScale)
        .shadow(colors.strokeBgL3, 6)
        .opacity(0)
        .z(Z_POINTER, 1)
        .build();
    const spBg = this.createImage(naviMap.convertImage)
        .scale(cursor.imgScale)
        .opacity(0)
        .z(Z_POINTER, 4)
        .build();
    this.createAnimation('bgD3.opacity')
        .whenUpdated(p => spBgD3.opacity(p.value))
        .update()
        .when(0, 0).when(800, 1).callback(() => spBgD3.style().z(Z_IMG, 1).update())
        .commit();
    this.createAnimation('bg.opacity')
        .whenUpdated(p => spBg.opacity(p.value))
        .update()
        .when(900, 0).when(1700, 1).callback(() => spBg.style().z(Z_IMG, 4).update())
        .commit();

    this.on(cEvent.pointMove, () => {
      spBgD3.style().z(Z_POINTER, 1).update();
      spBg.style().z(Z_POINTER, 4).update();
    });
    this.on(cEvent.pointArrive, () => {
      spBgD3.style().z(Z_IMG, 1).update();
      spBg.style().z(Z_IMG, 4).update();
    });
  }

  putInterestPts() {
    const pts = this.cursor.interestPts.concat().sort(() => Math.random() - 0.5);
    this.pts = pts.map((pt, index) => new InterestPoint({
      parent: this,
      x: pt.x, y: pt.y, delay: index * 400,
    }));

    this.timer()
        .when(pts.length * 400 + 800, () => this.emit(cEvent.pointMove))
        .commit();
  }
}

/**
 * zlevel=Z_POINTER
 * 1. 刚创建的时候有开始动画 stage=0
 * 2. 收到 pointArrive 事件时, 指针指到的点形态会变化 stage=1
 * 3. 收到 pointMove 事件时, 指针指到的点形态会变化 stage=2
 */
class InterestPoint extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);
    /** @type {number} */ this.delay = args.delay;

    /**
     * 0: 未打勾, zlevel=Z_Z_POINTER
     * 1: 指针正在指, zlevel=Z_IMG
     * 2: 打勾表示指针指到过了, zlevel=Z_Z_POINTER
     */
    this.stage = 0;
    this.on(cEvent.pointArrive, (_, value) => {
      if (value.x == this.x && value.y == this.y) {
        this.changeStage1();
      }
      this.spBorder && this.spBorder.style().z(Z_IMG, 15).update();
      this.spCore && this.spCore.style().z(Z_IMG, 15).update();
    });
    this.on(cEvent.pointMove, () => {
      if (this.stage == 1) {
        this.changeStage2();
      }
      this.spBorder && this.spBorder.style().z(Z_POINTER, 15).update();
      this.spCore && this.spCore.style().z(Z_POINTER, 15).update();
    });
  }

  init() {
    const {delay} = this;
    const {colors} = layout;
    const {width} = cLayout.map.intsPt;
    const w = width >> 1;
    const wi = (w * 0.6) | 0; // width of inner rect

    const spBorder = this.spBorder = this.createPathFromString(`M0-${w}l${w},${w}l-${w},${w}l-${w}-${w}Z`)
      .blend('lighter')
      .stroke(colors.strokeBgL2, 2)
      .fillNone()
      .z(Z_POINTER, 15)
      .build();
    spBorder.hide();
    const corePath = `M0-${wi}l${wi},${wi}l-${wi},${wi}l-${wi}-${wi}Z`;
    const spCore = this.spCore = this.createPathFromString(corePath)
      .blend('lighter')
      .fill(colors.strokeNormal)
      .z(Z_POINTER, 15)
      .build();
    spCore.hide();
    const spCoreWhite = this.createPathFromString(corePath)
      .stroke('#FFF', 6)
      .shadow(colors.strokeNormal, 6)
      .blend('lighter')
      .fill('#FFF')
      .z(Z_POINTER, 16)
      .build();
    spCoreWhite.hide();

    this.timer()
      .when(delay, () => {
        spBorder.show();
        spCore.show();
        spCoreWhite.show();

        this.createAnimation('op')
          .whenUpdated(p => spCoreWhite.opacity(p.value))
          .update()
          .when(0, 1)
          .when(600, 0).callback(() => spCoreWhite.destroy())
          .commit();
      })
      .commit();
  }

  changeStage1() {
    const {colors} = layout;
    this.stage = 1;

    this.spCore.opacity(0.6);

    // 涟漪 Ripples
    this.lastRippleTime = 0;
    this.rippleTickerCleaner = this.ticker(now => {
      if (now - this.lastRippleTime > 400) {
        this.lastRippleTime = now;
      } else {
        return;
      }

      const ripples = [
        {path: 'M0,0h10l-10-10Z',  x2: 0, y2: 1},
        {path: 'M0,0v-10l-10,10Z', x2: 1, y2: 1},
        {path: 'M0,0h-10l10,10Z',  x2: 1, y2: 0},
        {path: 'M0,0v10l10-10Z',   x2: 0, y2: 0},
      ].map(o => this.createPathFromString(o.path)
        .fillLinearGradient(0.5, 0.5, o.x2, o.y2, [
          {offset:0, color:colors.strokeD1},
          {offset:1, color:colors.strokeBg}
        ])
        .scale(0, 0)
        .opacity(0)
        .blend('lighter')
        .z(Z_POINTER, 10)
        .build());

      this.createAnimation('ripple-scale-' + now)
        .whenUpdated(p => ripples.forEach(ripple => ripple.scaleTo(p.value)))
        .update()
        .when(0, 0)
        .when(1000, 8).callback(() => {
          ripples.forEach(ripple => ripple.destroy());
          ripples.splice(0, ripples.length);
        })
        .commit();
      this.createAnimation('ripple-opacity-' + now)
        .whenUpdated(p => ripples.forEach(ripple => ripple.opacity(p.value)))
        .update()
        .when(0, 0)
        .when(200, 0.6)
        .when(500, 0.6).easing(hold)
        .when(1000, 0)
        .commit();
    });
  }

  changeStage2() {
    this.stage = 2;
    this.rippleTickerCleaner && this.rippleTickerCleaner.call(this);

    const {colors} = layout;
    const {width} = cLayout.map.intsPt;
    const w = ((width >> 2) * 0.6) | 0; 

    const corePath = `M0,${w/2}l${2.5*w}-${2.5*w}l${w/2},${w/2}l-${w*3},${w*3}l-${w*1.5}-${w*1.5}l${w/2}-${w/2}Z`;
    this.spCore.destroy();
    this.spCore = this.createPathFromString(corePath)
      .blend('lighter')
      .fill(colors.strokeNormal)
      .z(Z_POINTER, 15)
      .build();
    const spCoreWhite = this.createPathFromString(corePath)
      .blend('lighter')
      .fill(colors.strokeNormal)
      .z(Z_POINTER, 15)
      .build();
    this.createAnimation('op')
      .whenUpdated(p => spCoreWhite.opacity(p.value))
      .update()
      .when(0, 1)
      .when(600, 0).callback(() => spCoreWhite.destroy())
      .commit();
  }
}

/**
 * 地图上面的指针
 */
class Pointer extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    /** @type {Cursor} */ const cursor = this.cursor = args.cursor;
    this.useParentZRenderInstance(cursor.cursorX, cursor.cursorY);

    this.lastX = this.oriX = cursor.cursorX;
    this.lastY = this.oriY = cursor.cursorY;

    this.on(cEvent.pointMove, () => this.movePoint());
  }

  init() {
    const {colors} = layout;
    const {width} = cLayout.map.intsPt;
    const w = width >> 1;
    const w2 = (width * 0.9) | 0;
    const {cursor} = this;

    // 指针中心
    const path1 = `M0-${w}l${w},${w}l-${w},${w}l-${w}-${w}Z`;
    const perimeter1 = Math.hypot(w, w) * 4;
    const path2 = `M0-${w2}l-${w2},${w2}l${w2},${w2}l${w2}-${w2}Z`;
    const perimeter2 = Math.hypot(w2, w2) * 4;

    const spBorder1s = [
      {w1: perimeter1 / 8, color: colors.strokeNormal, z: 105 },
      {w1: perimeter1 / 3, color: colors.strokeD1,     z: 104 },
    ].map(o => this.createPathFromString(path1)
      .fillNone()
      .stroke(o.color, 2)
      .opacity(0)
      .lineDash([o.w1, perimeter1 / 2 - o.w1])
      .z(Z_POINTER, o.z)
      .build());

    const spBorder2s = [
      {w1: perimeter2 / 8, color: colors.strokeNormal, z: 105 },
      {w1: perimeter2 / 3, color: colors.strokeD1,     z: 104 },
    ].map(o => this.createPathFromString(path2)
      .fillNone()
      .stroke(o.color, 2)
      .opacity(0)
      .lineDash([o.w1, perimeter2 / 2 - o.w1])
      .z(Z_POINTER, o.z)
      .build());

    // 位置数据
    this.spXTx = this.createPathFromString(layout.buildTextPath('X:0').path)
      .fill(layout.colors.strokeD1)
      .z(Z_POINTER, 106)
      .position(10, 12)
      .opacity(0)
      .build();
    this.spYTx = this.createPathFromString(layout.buildTextPath('Y:0').path)
      .fill(layout.colors.strokeD1)
      .z(Z_POINTER, 106)
      .position(10, 26)
      .opacity(0)
      .build();
    this.ticker(() => {
      let {lastX, lastY} = this;
      const cursorX = cursor.cursorX | 0;
      const cursorY = cursor.cursorY | 0;

      if (lastX != cursorX) {
        this.lastX = cursorX;
        this.spXTx && this.spXTx.destroy();
        this.spXTx = this.createPathFromString(layout.buildTextPath('X:' + (cursorX - this.oriX)).path)
          .fill(layout.colors.strokeD1)
          .z(Z_POINTER, 106)
          .position(10, 12)
          .build();
      }
      if (lastY != cursorY) {
        this.lastY = cursorY;
        this.spYTx && this.spYTx.destroy();
        this.spYTx = this.createPathFromString(layout.buildTextPath('Y:' + (cursorY - this.oriY)).path)
          .fill(layout.colors.strokeD1)
          .z(Z_POINTER, 106)
          .position(10, 26)
          .build();
      }
    });

    // 标尺线
    const spLines = [
      [-14, 0, -layout.content.width, 0],
      [14, 0, layout.content.width, 0],
      [0, -14, 0, -layout.content.width],
      [0, 14, 0, layout.content.width],
    ].map(n => this.createLine(...n)
      .stroke(layout.colors.strokeD1, 1)
      .opacity(0)
      .z(Z_POINTER, 105)
      .build());

    this.createAnimation('opacity')
      .whenUpdated(p => {
        spBorder1s.forEach(sp => sp.opacity(p.value));
        spBorder2s.forEach(sp => sp.opacity(p.value));
        spLines.forEach(sp => sp.opacity(p.value));
        this.spXTx.opacity(p.value);
        this.spYTx.opacity(p.value);
      })
      .update().when(1000, 0).when(1800, 1).commit();
    this.createAnimation('init-scale', null)
      .whenUpdated(p => {
        spBorder1s.forEach(sp => sp.scaleTo(p.value));
        spBorder2s.forEach(sp => sp.scaleTo(p.value));
      })
      .update().when(1000, 3).when(1800, 1).commit();

    // 指针坐标数据
    this.ticker(now => {
      const offset = (now % 2000) / 2000;
      spBorder1s.forEach(sp => sp.style().lineDash(null, offset * perimeter1).update());
      spBorder2s.forEach(sp => sp.style().lineDash(null, offset * perimeter2).update());
    });
  }

  movePoint() {
    // 准备移动位置
    const { cursor } = this;

    const nextPt = cursor.prepareNextMove();
    const ox = cursor.cursorX;
    const oy = cursor.cursorY;
    const dx = Math.abs(ox - nextPt.x);
    const dy = Math.abs(oy - nextPt.y);

    let arriveCountDown = 2;
    let countDown = () => {
      arriveCountDown--;
      if (arriveCountDown == 0) this.emit(cEvent.pointArrive, nextPt);
    }

    this.createAnimation('xx').whenUpdated(p => {
      this.x = p.value;
      cursor.setCursor(p.value);
    }).update().when(0, ox).when(dx * 12, nextPt.x).callback(countDown).commit();
    this.createAnimation('yy').whenUpdated(p => {
      this.y = p.value;
      cursor.setCursor(undefined, p.value);
    }).update().when(0, oy).when(dy * 12, nextPt.y).callback(countDown).commit();
  }
}

/**
 * 缩略图框, 在地图指针指到位置时, 出现的画框.
 * 
 * 缩略图的 (x, y) 是框的中心点
 */
class ThumbnailFrame extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    // position TODO
    this.useParentZRenderInstance(300, 300); // 画框显示时将重新设置位置. 位置由上层调用方确定
    /** @type {Cursor} */ this.cursor = args.cursor;

    const {lineLAnchorPoses} = cLayout.map.thumbnail;
    const lineLAnchor0 = lineLAnchorPoses[0].concat();
    this.lineLAnchorX = lineLAnchor0[0];
    this.lineLAnchorY = lineLAnchor0[1];

    this.on(cEvent.pointArrive, (_, pt) => this.show(pt));
    this.on(cEvent.pointMove, () => this.hide());
  }

  init() {
    const {lineL1W, lineL2W, framePath1, frameBgPath} = cLayout.map.thumbnail;
    const {strokeNormal, strokeD1, strokeBgD2} = layout.colors;
    const {lineLAnchorX, lineLAnchorY} = this;

    // 1. 启动部分
    // 细线x4: (左上) spLs[0] spLs[1] (右下) spLs[2] spLs[3]
    // 粗线x4: (左上) spLs[4] spLs[5] (右下) spLs[6] spLs[7]
    // 所有启动部分的线都在启动部分结束后 hide
    this.spLParams = [
      {l:true,  first:true,  w:lineL1W, start0:0, end0:0.5, start1:0, end1:1, ax:lineLAnchorX,  ay:lineLAnchorY},
      {l:true,  first:false, w:lineL1W, start0:0, end0:0.5, start1:0, end1:1, ax:lineLAnchorX,  ay:lineLAnchorY},
      {l:false, first:true,  w:lineL1W, start0:0, end0:0.5, start1:0, end1:1, ax:-lineLAnchorX, ay:-lineLAnchorY},
      {l:false, first:false, w:lineL1W, start0:0, end0:0.5, start1:0, end1:1, ax:-lineLAnchorX, ay:-lineLAnchorY},
      {l:true,  first:true,  w:lineL2W, start0:1, end0:1,   start1:0, end1:1, ax:lineLAnchorX,  ay:lineLAnchorY},
      {l:true,  first:false, w:lineL2W, start0:1, end0:1,   start1:0, end1:1, ax:lineLAnchorX,  ay:lineLAnchorY},
      {l:false, first:true,  w:lineL2W, start0:1, end0:1,   start1:0, end1:1, ax:-lineLAnchorX, ay:-lineLAnchorY},
      {l:false, first:false, w:lineL2W, start0:1, end0:1,   start1:0, end1:1, ax:-lineLAnchorX, ay:-lineLAnchorY},
    ];

    this.spLs = this.spLParams.map(p => this.createPolygon(...this.generateLinePath(p.l, p.first, p.w, 0, 1))
      .strokeNone()
      .fill(strokeNormal)
      .position(p.ax, p.ay)
      .shadow(strokeNormal, p.w == lineL2W ? 5 : 0)
      .z(Z_UI_DYNAMIC, 55)
      .build());
    this.spLs.forEach(sp => sp.hide());

    // 2. 展开部分
    this.spFrame1 = this.createPathFromString(framePath1)
      .strokeNone()
      .fill(strokeD1)
      .shadow(strokeD1, 3)
      .z(Z_UI_DYNAMIC, 55)
      .build();
    this.spFrame1.hide();

    /** @type {Sprite} 每次生成不同的 */ this.spFrameSeq = null;
    /** @type {Sprite} 每次生成不同的 */ this.spFrame2 = null;
    this.spBg = this.createPathFromString(frameBgPath)
      .strokeNone()
      .fill(strokeBgD2)
      .z(Z_UI_DYNAMIC, 41)
      .build();
    this.spBg.hide();
  }

  /**
   * 1. 启动部分
   * @param {{x:number, y:number, name:string}} pt 
   */
  show(pt) {
    const {width, height, lineLAnchorPoses} = cLayout.map.thumbnail;
    const {spLs, spLParams, cursor} = this;

    let offX = (width * 0.6) | 0;
    offX = pt.x > cursor.imgWidth / 2 ? pt.x - offX : pt.x + offX;
    let offY = (height * 0.6) | 0;
    offY = pt.y > cursor.imgHeight / 2 ? pt.y - offY : pt.y + offY;
    if (offX > pt.x && offY > pt.y) { // 避免遮挡坐标部分
      offX += 30;
    }

    this.x = offX;
    this.y = offY;

    spLs.forEach((sp, i) => {
      sp.show();
      const params = spLParams[i];
      sp.polylineShape()
          .pointList(this.generateLinePath(params.l, params.first, params.l, params.start0, params.end0))
          .update()
          .style().z(Z_UI_DYNAMIC, 55).opacity(1).update();
    });
    // 细线部分
    this.createAnimation('l1.path').whenUpdated(p => {
      spLs.slice(0, 4).forEach((sp, i) => {
        const params = spLParams[i];
        sp.polylineShape()
            .pointList(this.generateLinePath(params.l, params.first, params.w,
                calc(params.start0, params.start1, p.value),
                calc(params.end0, params.end1, p.value)))
            .update();
      });
    }).update().when(0, 0).when(100, 1).easing(cubicOut).commit();
    // 粗线部分
    this.createAnimation('l2.path').whenUpdated(p => {
      spLs.slice(4, 8).forEach((sp, i) => {
        const params = spLParams[i + 4];
        sp.polylineShape()
            .pointList(this.generateLinePath(params.l, params.first, params.w,
                calc(params.start0, params.start1, p.value),
                calc(params.end0, params.end1, p.value)))
            .update();
      });
    }).update().when(100, 0).when(250, 1).easing(cubicOut).commit();
    // 位置
    this.createAnimation('l1.pos')
      .whenUpdated(p => this.moveSpLine(lineLAnchorPoses[0], lineLAnchorPoses[1], p.value))
      .update().when(0, 0).when(250, 1).easing(cubicOut).callback(() => this.showStage2(pt)).commit();
  }

  showStage2(pt) {
    const {lineLAnchorPoses, height, width, framePath2} = cLayout.map.thumbnail;
    const {spLs, spFrame1, spBg} = this;
    const {strokeNormal, strokeD1} = layout.colors;

    spLs.slice(0, 4).forEach(sp => sp.hide());
    this.createAnimation('l2.pos.stage2A')
      .whenUpdated(p => this.moveSpLine(lineLAnchorPoses[1], lineLAnchorPoses[0], p.value))
      .update().when(150, 0).when(250, 1).easing(cubicIn).commit();
    this.createAnimation('l2.pos.stage2B')
      .whenUpdated(p => this.moveSpLine(lineLAnchorPoses[0], lineLAnchorPoses[2], p.value))
      .update().when(250, 0).when(450, 1).easing(cubicOut).commit();

    // frame
    const seqTx = `#${(Math.random()*10) | 0}${(Math.random()*10) | 0}${(Math.random()*10) | 0}`;
    const seqPathOpt = layout.buildTextPath(seqTx);
    this.spFrameSeq && this.spFrameSeq.destroy();
    this.spFrame2 && this.spFrame2.destroy();
    const spFrameSeq = this.spFrameSeq = this.createPathFromString(seqPathOpt.path)
      .fill(strokeNormal)
      .strokeNone()
      .shadow(strokeNormal, 5)
      .z(Z_UI_DYNAMIC, 55)
      .position(width / 2 - 27 - seqPathOpt.width, -height / 2)
      .opacity(0)
      .build();
    const spFrame2 = this.spFrame2 = this.createPathFromString(framePath2(seqPathOpt.width))
      .fill(strokeD1)
      .strokeNone()
      .shadow(strokeD1, 3)
      .z(Z_UI_DYNAMIC, 55)
      .opacity(0)
      .build();

    spFrame1.show();
    spFrame1.opacity(0);
    spBg.show();
    spBg.opacity(0);
    this.createAnimation('spFrame1.opacity')
      .whenUpdated(p => {
        spFrame1.opacity(p.value);
        spFrame2.opacity(p.value);
        spFrameSeq.opacity(p.value);
        spBg.opacity(p.value * 0.5);
      })
      .update().when(400, 0).when(600, 1).commit();
  }

  hide() {
    const {lineLAnchorPoses} = cLayout.map.thumbnail;
    const {spLs, spFrame1, spFrameSeq, spFrame2, spBg} = this;

    spLs.slice(0, 4).forEach(sp => sp.hide());
    this.createAnimation('l2.pos.stage9A')
      .whenUpdated(p => this.moveSpLine(lineLAnchorPoses[2], lineLAnchorPoses[0], p.value))
      .update().when(150, 0).when(350, 1).easing(cubicIn).commit();
    this.createAnimation('l2.pos.stage9B')
      .whenUpdated(p => this.moveSpLine(lineLAnchorPoses[0], lineLAnchorPoses[1], p.value))
      .update().when(350, 0).when(450, 1).easing(cubicOut).commit();
    this.createAnimation('l2.opacity')
      .whenUpdated(p => spLs.forEach(sp => sp.opacity(p.value)))
      .update().when(500, 1).when(1000, 0).commit();
    this.createAnimation('spFrame1.opacity')
      .whenUpdated(p => {
        spFrame1.opacity(p.value);
        spBg.opacity(p.value * 0.5);
        spFrameSeq && spFrameSeq.opacity(p.value);
        spFrame2 && spFrame2.opacity(p.value);
      })
      .update().when(0, 1).when(200, 0).callback(() => {
        spFrame1.hide();
        spBg.hide();
        if (spFrameSeq) {
          spFrameSeq.destroy();
          this.spFrameSeq = null;
        }
        if (spFrame2) {
          spFrame2.destroy();
          this.spFrame2 = null;
        }
      }).commit();
  }

  /**
   * @param {boolean} isL true:左上 false:右下
   * @param {boolean} isFirst 每个部分由两条线段组成, true:斜线段, false:水平线
   * @param {number} lineWidth
   * @param {number} start 0~1
   * @param {number} end 0~1
   */
  generateLinePath(isL, isFirst, lineWidth, start, end) {
    const {lineLPts} = cLayout.map.thumbnail;

    const base1X = lineLPts[1][0];
    const base1Y = lineLPts[1][1];
    const base2X = isFirst ? lineLPts[0][0] : lineLPts[2][0];
    const base2Y = isFirst ? lineLPts[0][1] : lineLPts[2][1];

    let linePts = [[0,0],[0,0],[0,0],[0,0]];
    linePts[3][0] = linePts[0][0] = calc(base1X, base2X, start);
    linePts[3][1] = linePts[0][1] = calc(base1Y, base2Y, start);
    linePts[2][0] = linePts[1][0] = calc(base1X, base2X, end);
    linePts[2][1] = linePts[1][1] = calc(base1Y, base2Y, end);
    if (isFirst) {
      linePts[2][0] += lineWidth;
      linePts[3][0] += lineWidth;
    } else {
      const ww = ((lineWidth * 0.7) | 0) + 1;
      linePts[2][0] -= ww;
      linePts[3][0] -= ww;
      linePts[2][1] += ww;
      linePts[3][1] += ww;
    }

    if (!isL) {
      linePts.forEach((v, i) => {
        linePts[i][0] = -v[0];
        linePts[i][1] = -v[1];
      });
    }
    return linePts;
  }

  /**
   * @param {number[]} from 
   * @param {number[]} to 
   * @param {number} delta 
   */
  moveSpLine(from, to, delta) {
    const px = calc(from[0], to[0], delta);
    const py = calc(from[1], to[1], delta);
    this.spLs.forEach((sp, i) => {
      const params = this.spLParams[i];
      sp.moveTo(params.l ? px : -px, params.l ? py : -py);
    });
  }
}

/**
 * 上面部分的标题.
 * 组件锚点位置在开始的方块中心.
 */
class TopTitle extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);
    /** @type {number} */ this.h2 = (args.height / 2) | 0;
    /** @type {number} */ this.w2 = (args.width / 2) | 0;
    this.showState = false;

    this.on(cEvent.pointArrive, (_, v) => this.show(v.name));
    this.on(cEvent.pointMove, () => this.showState && this.hide());
  }

  init() {
    const { w2, h2 } = this;
    const { colors } = layout;
    const { h2:rh2 } = cLayout.topTitle.rect1;

    // Rect 部分
    this.spLine1l = this.createPolyline([0,-rh2],[-rh2,0],[0,rh2])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 111)
        .build();
    this.spLine1r = this.createPolyline([0,-rh2],[rh2,0],[0,rh2])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 111)
        .build();
    this.spLine1all = this.createPolygon([0,-rh2],[rh2,0],[0,rh2],[-rh2,0])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .fillNone()
        .opacity(0)
        .z(Z_UI_DYNAMIC, 111)
        .build();

    this.spCore = this.createPolygon([0,-rh2],[rh2,0],[0,rh2],[-rh2,0])
        .shadow(colors.strokeD1, 6)
        .fill(colors.strokeD1)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 111)
        .build();
    this.spCoreWhite = this.createPolygon([0,-rh2],[rh2,0],[0,rh2],[-rh2,0])
        .shadow('#FFF', 6)
        .fill('#FFF')
        .opacity(0)
        .z(Z_UI_DYNAMIC, 9)
        .build();

    // Border 部分
    const bw = 4;
    this.spBorder1 = this.createPolyline([-bw,-rh2-bw-bw],[-bw,-rh2-bw],[-rh2-bw,-bw],[-rh2-bw-bw,-bw])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 3)
        .build();
    this.spBorder2 = this.createPolyline([-bw,rh2+bw+bw],[-bw,rh2+bw],[-rh2-bw,bw],[-rh2-bw-bw,bw])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 3)
        .build();
    this.spBorder3 = this.createPolyline([bw,-rh2-bw-bw],[bw,-rh2-bw],[rh2+bw,-bw],[rh2+bw+bw,-bw])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 3)
        .build();
    this.spBorder4 = this.createPolyline([bw,rh2+bw+bw],[bw,rh2+bw],[rh2+bw,bw],[rh2+bw+bw,bw])
        .stroke(colors.strokeNormal, 2)
        .shadow(colors.strokeNormal, 6)
        .opacity(0)
        .z(Z_UI_DYNAMIC, 3)
        .build();

    const h3 = h2 / 3;
    this.spBg = this.createPolygon( // 左上角开始顺时针
          [-w2+h3*2,-h2],[w2-h3*2,-h2],[w2,-h2+h3*2],[w2,h2-h3*2],
          [w2-h3*2,h2],[-w2+h3*2,h2],[-w2,h2-h3*2],[-w2,-h2+h3*2]
        )
        .fill(colors.strokeBgD2)
        .opacity(0)
        .blend("darken")
        .z(Z_POINTER, 999)
        .build();
  }

  /**
   * 开始播放出现的动画
   * @param {string} text
   */
  show(text) {
    this.showRect(text);
    this.showBorder();
    this.showState = true;
  }

  /**
   * @param {string} text
   */
  showRect(text) {
    const { spLine1l, spLine1r, spLine1all, spCore, spCoreWhite } = this;
    const { h1:rectH1, h2:rectH2, marginR } = cLayout.topTitle.rect1;
    const { colors } = layout;

    spLine1l.moveTo(-4*rectH1, 0).opacity(0);
    spLine1r.moveTo(4*rectH1, 0).opacity(0);
    spLine1all.moveTo(0, 0).opacity(0);
    spCore.moveTo(0, 0).opacity(0);
    spCoreWhite.moveTo(0, 0).opacity(0);
    {
      let ww = rectH1 / 2 * 0.7;
      spCoreWhite.polylineShape().pointList([[0,-ww],[ww,0],[0,ww],[-ww,0]]).update();
    }

    const textScale = 1.5;
    const { path:textPath, width:textW, height:textH } = layout.buildTextPath(text);

    this.createAnimation('line1.opacity', 0)
        .whenUpdated(p => {
          spLine1l.opacity(p.value);
          spLine1r.opacity(p.value);
        })
        .update()
        .when(0, 0)
        .when(300, 1).easing(cubicOut).callback(() => spLine1all.opacity(1))
        .when(301, 0)
        .commit();
    this.createAnimation('line1.x', 0)
        .whenUpdated(p => {
          spLine1l.moveTo(-p.value);
          spLine1r.moveTo(p.value);
        })
        .update()
        .when(0, 4 * rectH1)
        .when(300, 0).easing(cubicOut)
        .commit();

    this.createAnimation('core.w')
        .whenUpdated(p => {
          const ww = p.value;
          spCore.polylineShape().pointList([[0,-ww],[ww,0],[0,ww],[-ww,0]]).update();
        })
        .update()
        .when(500, 0).callback(() => spCore.opacity(1))
        .when(600, rectH1 / 2 * 0.7)
        .commit();
    this.createAnimation('coreWhite.opacity')
        .whenUpdated(p => spCoreWhite.opacity(p.value))
        .update()
        .when(0, 0)
        .when(600, 1).easing(hold)
        .when(700, 1).easing(hold)
        .when(1000, 0)
        .commit();

    const allWidth = textW * textScale + marginR + rectH2;
    this.createAnimation('all1.x', 0)
        .whenUpdated(p => {
          spLine1all.moveTo(p.value);
          spCore.moveTo(p.value);
        })
        .update()
        .when(1100, 0)
        .when(1400, -allWidth / 2 + rectH2 / 2).easing(cubicInOut).callback(() => {
          spLine1all.style().z(Z_UI_STATIC).update();
          spCore.style().z(Z_UI_STATIC).update();
        })
        .commit();
    this.createAnimation('all1.w')
        .whenUpdated(p => {
          const ww = p.value;
          spLine1all.polylineShape().pointList([[0,-ww],[ww,0],[0,ww],[-ww,0]]).update();
          const ww2 = ww * 0.7;
          spCore.polylineShape().pointList([[0,-ww2],[ww2,0],[0,ww2],[-ww2,0]]).update();
        })
        .update()
        .when(1100, rectH1 / 2)
        .when(1500, rectH2 / 2).easing(cubicInOut)
        .commit();

    this.spTitle && this.spTitle.destroy() && (this.spTitle = null);
    const spTitle = this.spTitle = this.createPathFromString(textPath)
        .fill(colors.yellowNormal)
        .shadow(colors.yellowNormal, 6)
        .opacity(0)
        .position(allWidth / 2 - textW * textScale, -textH * textScale / 2)
        .scale(textScale, textScale)
        .z(Z_UI_DYNAMIC, 110)
        .build();
    this.createAnimation('title.opacity', 0)
        .whenUpdated(p => spTitle.opacity(p.value))
        .update()
        .when(1400, 0)
        .when(1500, 0.6).callback(() => spTitle.style().z(Z_UI_STATIC).update())
        .commit();
  }

  showBorder() {
    const { spBorder1, spBorder2, spBorder3, spBorder4, spBg, w2, h2 } = this;
    const { h1:rectH1 } = cLayout.topTitle.rect1;

    spBorder1.moveTo(0, 0).opacity(0);
    spBorder2.moveTo(0, 0).opacity(0);
    spBorder3.moveTo(0, 0).opacity(0);
    spBorder4.moveTo(0, 0).opacity(0);
    spBg.opacity(0);

    this.createAnimation('border.opacity', 0)
        .whenUpdated(p => {
          spBorder1.opacity(p.value);
          spBorder2.opacity(p.value);
          spBorder3.opacity(p.value);
          spBorder4.opacity(p.value);
        })
        .update()
        .when(200, 0)
        .when(300, 1)
        .when(600, 0.3).easing(delay)
        .when(1000, 0.3).easing(hold)
        .when(1100, 1).easing(delay)
        .commit();
    this.createAnimation('border.x', 0)
        .whenUpdated(p => {
          spBorder1.moveTo(-p.value);
          spBorder2.moveTo(-p.value);
          spBorder3.moveTo(p.value);
          spBorder4.moveTo(p.value);
        })
        .update()
        .when(200, rectH1*4)
        .when(600, 0).easing(cubicOut)
        .when(850, 0).easing(hold)
        .when(1500, w2 - h2).easing(cubicInOut)
        .commit();

    const h3 = h2 / 3;
    this.createAnimation('bg.w')
        .whenUpdated(p => {
          const ww = p.value;
          spBg.polylineShape().pointList([
            [-ww+h3*2,-h2],[ww-h3*2,-h2],[ww,-h2+h3*2],[ww,h2-h3*2],
            [ww-h3*2,h2],[-ww+h3*2,h2],[-ww,h2-h3*2],[-ww,-h2+h3*2]
          ]).update();
        })
        .update()
        .when(850, h2)
        .when(1500, w2).easing(cubicInOut).callback(() => spBg.style().z(Z_UI_STATIC, 108).update())
        .commit();
    this.createAnimation('bg.opacity')
        .whenUpdated(p => spBg.opacity(p.value))
        .update()
        .when(1000, 0)
        .when(1100, 0.6)
        .commit();
  }

  hide() {
    this.hideRect();
    this.hideBorder();
    this.showState = false;
  }

  hideRect() {
    const { spLine1all, spCore, spTitle } = this;
    const { h1:rectH1, h2:rectH2 } = cLayout.topTitle.rect1;

    if (spTitle) {
      this.createAnimation('title.opacity', 0)
          .whenUpdated(p => spTitle.opacity(p.value))
          .update()
          .when(0, 0.6).callback(() => spTitle.style().z(Z_UI_DYNAMIC).update())
          .when(200, 0)
          .commit();
    }
    if (spLine1all) {
      this.createAnimation('all1.x', 0)
          .whenUpdated(p => {
            spLine1all.moveTo(p.value);
            spCore.moveTo(p.value);
          })
          .update()
          .when(200, spLine1all.x).callback(() => spLine1all.style().z(Z_UI_DYNAMIC).update())
          .when(500, 0).easing(cubicInOut)
          .commit();
      this.createAnimation('all1.w')
          .whenUpdated(p => {
            const ww = p.value;
            spLine1all.polylineShape().pointList([[0,-ww],[ww,0],[0,ww],[-ww,0]]).update();
            const ww2 = ww * 0.7;
            spCore.polylineShape().pointList([[0,-ww2],[ww2,0],[0,ww2],[-ww2,0]]).update();
          })
          .update()
          .when(200, rectH2 / 2).callback(() => spCore.style().z(Z_UI_DYNAMIC).update())
          .when(500, rectH1 / 2).easing(cubicInOut)
          .commit();
      this.createAnimation('core.opacity')
          .whenUpdated(p => spCore.opacity(p.value))
          .update()
          .when(800, 1)
          .when(1300, 0)
          .commit();
      this.createAnimation('all1.opacity')
          .whenUpdated(p => spLine1all.opacity(p.value))
          .update()
          .when(1000, 1)
          .when(1500, 0)
          .commit();
    }
  }

  hideBorder() {
    const { spBorder1, spBorder2, spBorder3, spBorder4, spBg, w2, h2 } = this;

    if (spBorder1) {
      this.createAnimation('border.opacity', 0)
          .whenUpdated(p => {
            spBorder1.opacity(p.value);
            spBorder2.opacity(p.value);
            spBorder3.opacity(p.value);
            spBorder4.opacity(p.value);
          })
          .update()
          .when(200, 1)
          .when(500, 0)
          .commit();
      this.createAnimation('border.x', 0)
          .whenUpdated(p => {
            spBorder1.moveTo(-p.value);
            spBorder2.moveTo(-p.value);
            spBorder3.moveTo(p.value);
            spBorder4.moveTo(p.value);
          })
          .update()
          .when(200, w2 - h2)
          .when(700, 0).easing(cubicInOut)
          .commit();
    }

    const h3 = h2 / 3;
    if (spBg) {
      this.createAnimation('bg.w')
          .whenUpdated(p => {
            const ww = p.value;
            spBg.polylineShape().pointList([
              [-ww+h3*2,-h2],[ww-h3*2,-h2],[ww,-h2+h3*2],[ww,h2-h3*2],
              [ww-h3*2,h2],[-ww+h3*2,h2],[-ww,h2-h3*2],[-ww,-h2+h3*2]
            ]).update();
          })
          .update()
          .when(200, w2)
          .when(700, h2).easing(cubicInOut)
          .commit();
      this.createAnimation('bg.opacity')
          .whenUpdated(p => spBg.opacity(p.value))
          .update()
          .when(300, 0.6).callback(spBg.style().z(Z_POINTER, 999).update())
          .when(450, 0)
          .commit();
    }
  }
}

/**
 * 下面的内容栏
 * 组件锚点位置在内容栏左上角. x 永远为 0
 */
class NaviDetail extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(0, cLayout.detail.y);

    /** @type {Cursor} */ this.cursor = args.cursor;
    /** @type {DeskAnimation} */ this.deskAnimation = args.deskAnimation;
  }

  init() {
    const { width, height } = layout.content;
    const { colors } = layout;

    const spBg = this.spBg = this.createRect(width, height)
      .fillLinearGradient(0, 0, 0, 0.01, [
        {offset:0, color:'#0000'},
        {offset:1, color:colors.strokeBgD3}
      ])
      .opacity(0)
      .z(Z_UI_DYNAMIC, 30)
      .build();
    const marginL = (width * 0.1) | 0;
    const marginT = 10;
    const lineW1 = 8;
    const spLine = this.createPolyline(
        [marginL,marginT+lineW1],[marginL+lineW1,marginT],
        [width-marginL-lineW1,marginT],[width-marginL,marginT+lineW1],
      )
      .stroke(colors.strokeD1, 2)
      .opacity(0)
      .z(Z_UI_DYNAMIC, 31)
      .build();

    this.createAnimation('bg.y')
      .whenUpdated(p => { spBg.moveTo(0, p.value); spLine.moveTo(0, p.value); })
      .update().when(600, height - this.y).when(1100, 0).easing(cubicOut).commit();
    this.createAnimation('bg.opacity')
      .whenUpdated(p => { spBg.opacity(p.value); spLine.opacity(p.value); })
      .update().when(600, 0).when(1100, 0.7).easing(cubicOut)
      .callback(_ => {
        spBg.style().z(Z_UI_STATIC, 30).update();
        spLine.style().z(Z_UI_STATIC, 31).update();
      })
      .commit();

    this.timer()
      .when(1500, () => this.freqIdc = new FreqIndicator({
        parent: this, cursor: this.cursor,
      }))
      .when(2000, () => this.freqIdc = new ResourceDetector({
        parent: this, cursor: this.cursor, deskAnimation: this.deskAnimation,
      }))
      .commit();
  }
}

/**
 * 下面内容栏的频率数值指示器,
 * 组件锚点位置在内容栏左上角.
 */
class FreqIndicator extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    const { x, y } = cLayout.detail.freqIdc;
    this.useParentZRenderInstance(x, y);

    /** @type {Cursor} */ this.cursor = args.cursor;
    this.meter = new Meter(this.cursor);

    this.on(cEvent.pointArrive, () => this.moveXTo(x - cLayout.detail.tranX));
    this.on(cEvent.pointMove, () => this.moveXTo(x));
  }

  init() {
    const { meter, cursor } = this;
    const { rectH } = cLayout.detail.freqIdc;
    const { colors } = layout;

    const w = meter.meters.length * 6 - 4;

    // 上面的频率柱状图
    const colorTx1 = parseColorText(colors.strokeBg).setA(77).toHexRGBA();
    const colorTx2 = parseColorText(colors.strokeBg).setA(25).toHexRGBA();
    const meters = this.meters = new Array(meter.meters.length).fill().map((_, index) => {
      const xx = index * 6;

      const spRectBg = this.createRect(2, rectH)
        .fillLinearGradient(0, 0.7, 0, 0.2, [
          {offset:0, color:colors.strokeBg},
          {offset:0.3, color:colorTx1},
          {offset:1, color:colorTx2},
        ])
        .z(Z_UI_DYNAMIC, 62)
        .position(xx, 0)
        .opacity(0)
        .build();
      const spRectTop = this.createRect(2, 2)
        .fill(colors.strokeD1)
        .z(Z_UI_DYNAMIC, 63)
        .position(xx, -2)
        .opacity(0)
        .build();

      const spRectV = this.createRect(2, rectH)
        .fillLinearGradient(0, 0.6, 0, 0.2, [
          {offset:0, color:colors.strokeD1},
          {offset:1, color:colors.yellowNormal},
        ])
        .z(Z_UI_DYNAMIC, 71)
        .position(xx, 0)
        .opacity(0)
        .build();
      const spVClip = this.createRect(2, rectH)
        .defineAsClip()
        .build();
      spRectV.setClipPath(spVClip);

      return { spRectBg, spRectTop, spRectV, spVClip };
    });
    this.createAnimation('opacity1')
      .whenUpdated(p => {
        this.meters.forEach(m => {
          m.spRectBg.opacity(p.value);
          m.spRectTop.opacity(p.value);
          m.spRectV.opacity(p.value);
        });
      })
      .update().when(0, 0).when(800, 1).callback(_ => {
        this.meters.forEach(m => {
          m.spRectBg.style().z(Z_UI_STATIC, 62).update();
          m.spRectTop.style().z(Z_UI_STATIC, 63).update();
        });
      }).commit();

    // 下面的刻度盘
    const spHLine = this.spHLine = this.createRect(w, 2)
      .fill(colors.strokeBg)
      .z(Z_UI_DYNAMIC, 62)
      .position(0, rectH + 4)
      .opacity(0)
      .build();
    const hpW = 2;
    const spHp1 = this.spHp1 = this.createRect(hpW, 2)
      .fill(colors.strokeD1)
      .z(Z_UI_DYNAMIC, 63)
      .position(0, rectH + 4)
      .opacity(0)
      .build();
    const spHp2 = this.spHp2 = this.createRect(hpW, 2)
      .fill(colors.strokeD1)
      .z(Z_UI_DYNAMIC, 63)
      .position(w - 2, rectH + 4)
      .opacity(0)
      .build();
    const sliderH = 24;
    const sliderY = rectH + 14;
    const spArrow1 = this.spArrow1 = this.createPolyline([sliderH/8,-sliderH/4],[-sliderH/8,0],[sliderH/8,sliderH/4])
      .stroke(colors.strokeD1, 2)
      .shadow(colors.strokeNormal, 6)
      .z(Z_UI_DYNAMIC, 63)
      .position(0, sliderY + sliderH / 2)
      .opacity(0)
      .build();
    const spArrow2 = this.spArrow2 = this.createPolyline([-sliderH/8,-sliderH/4],[sliderH/8,0],[-sliderH/8,sliderH/4])
      .stroke(colors.strokeD1, 2)
      .shadow(colors.strokeNormal, 6)
      .z(Z_UI_DYNAMIC, 63)
      .position(w, sliderY + sliderH / 2)
      .opacity(0)
      .build();

    const hSliderW = 24;
    const hSliderMax = meter.calcPos(cursor.imgWidth, cursor.imgHeight);
    const spHSlider = this.createRect(hSliderW, 2)
      .fill(colors.strokeD1)
      .shadow(colors.strokeNormal, 6)
      .position(hpW, rectH + 4)
      .opacity(0)
      .z(Z_UI_DYNAMIC, 63)
      .build();
    const divOffX = (w * 0.1) | 0;
    const divW = (w * 0.8) | 0;
    let divLinesOpacity = 0;
    const spDivLines = new Array(5).fill().map(() => {
      return this.createLine(0, 0, 0, sliderH)
        .opacity(divLinesOpacity)
        .position(divOffX, sliderY)
        .z(Z_UI_DYNAMIC, 65)
        .build();
    });

    this.createAnimation('opacity2')
      .whenUpdated(p => {
        divLinesOpacity = p.value;
        spHLine.opacity(p.value);
        spHp1.opacity(p.value);
        spHp2.opacity(p.value);
        spArrow1.opacity(p.value);
        spArrow2.opacity(p.value);
        spHSlider.opacity(p.value);
      })
      .update().when(400, 0).when(1200, 1).callback(_ => {
        this.moveSpriteToZ(Z_UI_STATIC);
      }).commit();

    this.ticker(now => {
      const changed = meter.calc();

      // 柱状图
      const vs = meter.meters.concat();
      meters.forEach((m, index) => {
        const v = vs[index] +
            0.03 * Math.sin((now + index * 103) / 300) +
            0.02 * Math.sin((index * 88 - now) / 400);
        m.spVClip.rectShape().height(v * rectH).y((1 - v) * rectH).update();
      });

      // 刻度盘
      // 显示区域: meter.position ~ meter.position+100, 每 20 为一刻度
      if (changed || (divLinesOpacity < 1)) {
        const unit = 20;
        const pos = meter.position;
        const d0 = Math.ceil(pos / unit) * unit;
        const dw = unit * spDivLines.length;
        const ds = spDivLines.map((_, index) => ((d0 + unit * index) - pos) / dw * Math.PI);

        spDivLines.forEach((sp, index) => {
          const value = ds[index];
          const xx = (1 - Math.cos(value)) / 2 * divW;
          sp.lineShape().x1(xx).x2(xx).update()
            .style()
            .opacity(divLinesOpacity * Math.sin(value))
            .stroke(colors.strokeNormal, 2 + 2 * Math.sin(value))
            .update();
        });

        // slider
        const ww = w - hpW - hpW - hSliderW;
        spHSlider.rectShape().x(pos / hSliderMax * ww).update();
      }
    });
  }

  moveXTo(toX) {
    this.moveSpriteToZ(Z_UI_DYNAMIC);
    const oriX = this.x;

    if (oriX == toX) return;

    this.getAnimX().update()
      .when(0, oriX)
			.when(400, toX).easing(cubicInOut).callback(() => this.moveSpriteToZ(Z_UI_STATIC))
			.commit();
  }

  moveSpriteToZ(z) {
    const {spHLine, spHp1, spHp2, spArrow1, spArrow2} = this;
    spHLine.style().z(z, 62).update();
    spHp1.style().z(z, 63).update();
    spHp2.style().z(z, 63).update();
    spArrow1.style().z(z, 63).update();
    spArrow2.style().z(z, 63).update();
  }
}

class P {
  /**
   * @param {number} x 
   * @param {number} y 
   */
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  equals(p) {
    return this.x === p.x && this.y === p.y;
  }
  clone() {
    return pt(this.x, this.y);
  }
  add(p) {
    return pt(this.x + p.x, this.y + p.y);
  }
}

function pt(x, y) { return new P(~~x, ~~y); }

/**
 * 下面内容栏的资源探测器,
 * 组件锚点位置在中心图形, 那个显示百分比的图形中心
 */
class ResourceDetector extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    const { x, y } = cLayout.detail.resDetect;
    this.useParentZRenderInstance(x, y);

    /** @type {DeskAnimation} */ this.deskAnimation = args.deskAnimation;
    /** @type {Cursor} */ this.cursor = args.cursor;

    this.hexNeighbors = [ pt(-1,-1),pt(0,-1),pt(1,0),pt(1,1),pt(0,1),pt(-1,0) ];
    this.hexExcludes = [ pt(0,0),pt(1,0),pt(0,-1),pt(-1,-1) ];
    /** @type {ResourceHexagon[]} */ this.hexs = [];

    this.on(cEvent.pointArrive, () => this.show());
    this.on(cEvent.pointMove, () => this.moveXTo(x));
  }

  // 画六边形
  init() {
    const { hexDx, hexW } = cLayout.detail.resDetect;
    const { strokeBg, strokeNormal } = layout.colors;

    const bashHex = calcHexagonPts(0, -0.5, hexDx * 2 + hexW);
    this.baseHexPos = [(bashHex[0][0] + bashHex[3][0]) / 2, (bashHex[0][1] + bashHex[3][1]) / 2];
    const hexPerimeter = this.hexPerimeter = bashHex.reduce((p, c, index, array) => {
      let p1 = c, p2 = array[(index + 1) % array.length];
      return p + Math.hypot(p2[0] - p1[0], p2[1] - p1[1]);
    }, 0);
    const hexOffset = this.hexOffset = hexDx + hexW / 2;

    this.spBaseHex = this.createPolygon(...bashHex)
      .stroke(strokeNormal, 1)
      .lineDash([0, hexPerimeter], hexOffset)
      .fill(strokeBg)
      .z(Z_UI_DYNAMIC, 30)
      .build();
    /** @type {Sprite} */ this.spBaseHexText = null;
  }

  show() {
    const { spBaseHex, hexPerimeter, hexOffset, hexs, baseHexPos } = this;
    const { resDetect, tranX } = cLayout.detail;

    // clean
    const hexNeighbors = this.hexNeighbors.map(h => h.clone());
    const hexExcludes = this.hexExcludes.map(h => h.clone());
    hexs.forEach(hex => hex.destroy());
    hexs.splice(0, hexs.length);

    // spBaseHex
    spBaseHex.style().lineDash([0, hexPerimeter], hexOffset).update();

    const MAX_TIME = 3500;
    this.moveXTo(resDetect.x - tranX);
    this.createAnimation('lineDash')
      .whenUpdated(p => {
        spBaseHex.style().lineDash([p.value, hexPerimeter - p.value], hexOffset).update();

        if (this.spBaseHexText) {
          this.spBaseHexText.destroy();
        }
        let percentage = ((p.value / hexPerimeter) * 100) | 0;
        const { path:textPath, width:textW, height:textH } = layout.buildTextPath(String(percentage));
        this.spBaseHexText = this.createPathFromString(textPath)
          .fill(percentage == 100 ? layout.colors.yellowNormal : layout.colors.strokeNormal)
          .position(baseHexPos[0] - textW/2, baseHexPos[1] - textH/2)
          .shadow(percentage == 100 ? layout.colors.yellowNormal : layout.colors.strokeNormal, 3)
          .z(Z_UI_DYNAMIC, 31)
          .build();
      })
      .update()
      .when(500, 0)
      .when(1500, hexPerimeter/2)
      .when(MAX_TIME, hexPerimeter).easing(cubicOut)
      .commit();

    this.timer().when(5000, () => this.hide()).commit();

    // ResourceHexagon
    let time = 500;
    /** @type {P[]} */ let nextHexs = [];
    hexExcludes.forEach(h => {
      const nexts = hexNeighbors.map(hh => h.add(hh))
        .filter(hh => !hexExcludes.some(hhh => hhh.equals(hh)))
        .filter(hh => !nextHexs.some(hhh => hhh.equals(hh)));
      nextHexs.push(...nexts);
    });

    while (time < MAX_TIME) {
      if (nextHexs.length === 0) {
        break;
      }

      // 随机挑一个
      let chosenIndex = (nextHexs.length * Math.random()) | 0;
      let chosen = nextHexs[chosenIndex];
      nextHexs.splice(chosenIndex, 1);
      hexExcludes.push(chosen);

      const nexts = hexNeighbors.map(hh => chosen.add(hh))
        .filter(hh => !hexExcludes.some(hhh => hhh.equals(hh)))
        .filter(hh => !nextHexs.some(hhh => hhh.equals(hh)))
        .filter(hh => hh.x > -7)
        .filter(hh => hh.x < 7)
        .filter(hh => hh.y >= hh.x / 2 - 2.5)
        .filter(hh => hh.y <= hh.x / 2 + 2);
      nextHexs.push(...nexts);

      this.timer()
        .when(time, () => hexs.push(new ResourceHexagon({ parent: this, xx: chosen.x, yy: chosen.y })))
        .commit();

      time += 10;
      time += ((Math.random() * 20) ** 2) | 0;
    }
  }

  hide() {
    this.hexs.forEach(h => h.readyToHide());
    // hide 动作完成后再进行下面的切换动作. 到时候修改 1000 这个值
    this.timer().when(600, () => this.close()).commit();
  }

  close() {
    if (this.cursor.currentIndex == 2) {
      this.deskAnimation.footerSwitch();
    } else {
      this.emit(cEvent.pointMove);
    }
  }

  moveXTo(toX) {
    // this.moveSpriteToZ(Z_UI_DYNAMIC);
    const oriX = this.x;

    if (oriX == toX) return;

    this.getAnimX().update()
      .when(0, oriX)
			.when(400, toX).easing(cubicInOut)/*.callback(() => this.moveSpriteToZ(Z_UI_STATIC))*/
			.commit();
  }
}

class ResourceHexagon extends ZRenderComp {
  constructor(args) {
    super(args.parent);

    this.xx = args.xx;
    this.yy = args.yy;
    const nameList = ['Fe', 'Si', 'C', 'H', 'Cu', 'Sn', 'Ti'];
    this.name = nameList[(Math.random() * nameList.length) | 0];
    this.useParentZRenderInstance();
  }

  init() {
    const { xx, yy, name } = this;
    const { strokeNormal } = layout.colors;
    const firstShape = calcHexagonPts(xx, yy, 0);

    this.spHex = this.createPolygon(...firstShape)
      .stroke(strokeNormal, 1)
      .fill(strokeNormal)
      .z(Z_UI_DYNAMIC, 50)
      .build();
    this.spHex.hide();

    const centerX = (firstShape[0][0] + firstShape[3][0]) / 2;
    const centerY = (firstShape[0][1] + firstShape[3][1]) / 2;
    const { path:textPath, width:textW, height:textH } = layout.buildTextPath(name);
    const spTitle = this.spTitle = this.createPathFromString(textPath)
      .fill(strokeNormal)
      .position(centerX - textW / 2, centerY - textH / 2)
      .z(Z_UI_DYNAMIC, 51)
      .build();
    spTitle.hide();

    this.show();
  }

  show() {
    const {xx, yy, spHex, spTitle} = this;

    spHex.show();
    this.createAnimation('width', 0)
      .whenUpdated(p => {
        const shape = calcHexagonPts(xx, yy, p.value);
        spHex.polylineShape().pointList(shape).update();
      })
      .update()
      .when(0, 0)
      .when(300, cLayout.detail.resDetect.hexW).easing(cubicOut).callback(() => {
        spHex.style().fillNone().update();
        spTitle.show();
      })
      .commit();
  }

  readyToHide() {
    const { xx, yy } = this;
    this.timer().when((Math.abs(xx) + Math.abs(yy)) * 70, () => this.hide()).commit();
  }

  hide() {
    const { xx, yy } = this;
    const { spHex, spTitle } = this;
    const { strokeNormal } = layout.colors;

    spHex.style().fill(strokeNormal).update();
    spTitle.hide();

    this.createAnimation('width', cLayout.detail.resDetect.hexW)
      .whenUpdated(p => {
        const shape = calcHexagonPts(xx, yy, p.value);
        spHex.polylineShape().pointList(shape).update();
      })
      .update()
      .when(0, cLayout.detail.resDetect.hexW)
      .when(300, 0).easing(cubicIn).callback(() => this.destroy())
      .commit();
  }
}

/**
 * @param {number} xx 六边形 x, 单位不是位置
 * @param {number} yy 六边形 y, 单位不是位置
 * @param {number} width 图形最上面那条边的宽 dx
 */
function calcHexagonPts(xx, yy, width) {
  const { hexH, hexDx, hexDy } = cLayout.detail.resDetect;
  const x = xx * hexDx;
  const y = (yy - xx / 2) * hexDy;

  const w = width / 2;
  const h = hexH / 2;

  return [
    [x+w,     y-h], // 右上角的点, 顺时针
    [x+w+h/2, y  ],
    [x+w,     y+h],
    [x-w,     y+h],
    [x-w-h/2, y  ],
    [x-w,     y-h],
  ];
}

export {
  NaviContent
};
